Reducer を REST のリソース単位で分割する
#Redux の Reducer をどういう単位で分割するかという命題は以前からある。 前提として Reducer は互いに独立した存在なので、Reducer をまたいだ状態遷移は基本的に書けない。
それを理解しないと、バックエンドのテーブル定義をそのまま踏襲して使いづらくなってしまうといったことが起こる。
逆に、Reducer は画面単位を基本にするという考えもあるが、これもこれで難しいと言うか、そもそも SPA で複数ページで共有するために Redux を導入したのにそれでは意味ないじゃないかと聞き返されそうである( また「フォームの状態は Redux に入れるべきでない」といったことはこのルールでは説明できないように見える )。
個人的に、Redux の reducer は #REST API におけるリソース分割と同じに考えるのが良いと思っている。DB のテーブルよりは大きく、画面単位よりは細かい切り方だ(ただしページの単位とリソースの単位はしばしば一致するので、結果的に画面単位できるのと変わらない形式になることもある。たとえば /items の一覧ページは itemReducer しか使わないとか )。 これは裏を返すと、API が REST(少なくとも RESTish)でないところに Redux を持ち込んでも嬉しさが少ないという話に見えそうだが、実際そのとおりだと思っている(たとえば GraphQL を使っているシチュエーションなら Redux は不要だろうし )。
なんだかんだで Redux は「(REST) API から取ったモデルを ViewModel にする」というパラダイムの産物だと思う。
例として、ブログのようなアプリケーションを考える。この時、Post(記事)と Comment(コメント)は同じ reducer に入れるべきだろうか?と答えたら、それらにまつわる API を自分ならどう設計するかを考えれば良い。
大抵の場合、コメントは記事の サブリソース だ。コメントは記事の子要素としてのみ現れる概念であり、記事から離れて「コメント一覧」というページを作ることは通常存在しない。/posts/1/comments.json はありうるし、posts/1.json に comments が生えていることもありうるが、/comments.json を独立して作るケースはちょっと考えにくい。ということは、comments は posts を入れる reducer の一部であるべきなんだ、という考えになる。もっというと、コメントを参照する方法が post.comments.map(...) 以外にないなら、正規化も別に必須ではない。
code:typescript
interface Post {
id: number
title: string
body: string
comments: Comment[]
}
interface State {
posts: Record<Post'id', Post> }
ただ、万が一記事から独立してコメント一覧を表示したい(たとえば「あなたのコメント履歴」ページを作りたくなった)場合、上の設計では苦しくなる。そういうときも別に reducer を分ける必要はなく、ただ正規化して対応すれば良い。
code:typescript
interface Post {
id: number
title: string
body: string
comment_ids: Comment'id'[] }
interface Comment {
id: number
user_id: number
body: string
}
interface State {
posts: Record<Post'id', Post> comments: Record<Comment'id', Comment> }
クライアントサイドの状態設計をする際は、常に「このモデルは親になることがありうるか?」「親から独立してそれ単体で一覧や詳細を取得するシチュエーションがあるか?」を自問するとよい。それがありうるモデルだけを reducer の主語にすると上手くユースケースに応じてまとまりやすい。「こいつは他のモデルのサブリソース以外にならないんじゃないか?」と思ったら json の 1 プロパティにとどめた方がよく、それは API 側も大抵そうなっているはずだ。